home *** CD-ROM | disk | FTP | other *** search
/ MacFormat 2000 July / macformat-092.iso / Dreamweaver 3 / Configuration / Commands / Clean Up HTML.js < prev    next >
Encoding:
Text File  |  1999-12-01  |  34.8 KB  |  1,085 lines

  1. //
  2. // Copyright 1999 Macromedia, Inc. All rights reserved. 
  3. // ----------------------------------------------------
  4. //
  5. // Clean Up HTML.js
  6. //
  7. // This command cleans up certain categories of superfluous
  8. // HTML within the user document without effecting document
  9. // layout.  This command makes two passes over the users 
  10. // document depending on the options selected; see 
  11. // cleanUpDocument() for more details.
  12. //
  13. // Version 3.0
  14. // Added new array class for cleanup HTML contains function.
  15. // ----------------------------------------------------
  16.  
  17. //
  18. // Global variables -- see initialize() for more comments.  
  19. // Dreamweaver doesn't currently initialize any globals 
  20. // loaded in auxilliary scripts through <SCRIPT SRC=..>, 
  21. // so we explicitly initialize these initialize().
  22. //
  23. var helpDoc = MM.HELP_cmdCleanUpHTML;
  24.  
  25. var cbRemoveEmptyTags;
  26. var cbRemoveRedundant;
  27. var cbCombineFonts;
  28. var cbRemoveTags;
  29. var cbRemoveComments;
  30. var cbRemoveDWComments;
  31. var cbShowLog;
  32. var tbTagsToRemove;
  33. var numEmptyRemoved;
  34. var numRedundantRemoved;
  35. var numTagsRemoved;
  36. var numCommentsRemoved;
  37. var numFontsCombined;
  38. var arrTagsToRemove = new Array(); // Array
  39. var strClassAttrib;
  40. var strStyleAttrib;
  41. var arrDWCommentTags = new Array();
  42. var emptyRemovalCandidates = new Array();
  43. var redundantTagCandidates = new Array();
  44. var combinableTagCandidates = new Array();
  45. var bPreserveEmptyHeader;
  46. var bRemovedTracing;
  47.  
  48.  
  49. //------------------ Commands API --------------------
  50.  
  51. function commandButtons()
  52. {
  53.    return new Array( BTN_OK,     "cleanUpDocument()"  // main entry point
  54.                    , BTN_Cancel, "window.close()"
  55.                    , BTN_Help,   "displayHelp()");
  56. }
  57.  
  58. function canAcceptCommand()
  59. {
  60.   return (dw.getFocus() == 'document');
  61. }
  62.  
  63. //
  64. // ------- Local Clean Up command functions ----------
  65. //
  66.  
  67. function arrayContains( curArr, item )
  68. {
  69.    var nElements = curArr.length;
  70.    for( var i = 0; i < nElements; i++ )
  71.       if ( curArr[i] == item )
  72.          return true;
  73.  
  74.    return false;   
  75. }
  76.  
  77. function isQuote( c )
  78. {
  79.    return( c == '\"' || c == '\'' );
  80. }
  81.  
  82. function isAlpha( c )
  83. {
  84.    var isoval = c.charCodeAt(0);
  85.    return( (isoval >= "A".charCodeAt(0) && isoval <= "Z".charCodeAt(0)) ||
  86.            (isoval >= "a".charCodeAt(0) && isoval <= "z".charCodeAt(0)));
  87. }              
  88.  
  89. // Match a <xxx or </xxx tag; note that xxx must be alphabetical
  90. function isTagBegin( currentchar, nextchar )
  91. {
  92.    return( currentchar == '<' && (isAlpha( nextchar ) || nextchar == '/') );
  93. }
  94.  
  95. // Note that '>' should be ignored within quotes inside tag brackets
  96. function isTagEnd( c )
  97. {
  98.    return( c == '>' );
  99. }
  100.  
  101. function isWhite( c )
  102. {
  103.    return( c == ' ' || c == '\t' || c == '\n' || c == '\r' );
  104. }
  105.  
  106. function isAllWhite( str )
  107. {
  108.    for( var i = 0; i < str.length; i++ )
  109.    {
  110.       if ( !isWhite( str.charAt( i ) ) )
  111.          return( false );
  112.    }
  113.    
  114.    return( true ); 
  115. }
  116.  
  117. // parseAttributes()
  118. //
  119. // Parse the attributes from within the start tag of a given
  120. // node per the rules found here: http://www.w3.org/TR/WD-html-lex/
  121. //    
  122. // Return an array of arrays (unfortunately; the associative 
  123. // aspect of arrays is overloaded with "instance properties",
  124. // so arrays already contain prototype methods/properties
  125. // value pairs):
  126. //
  127. //          arr[0] --> attributes array
  128. //          arr[1] --> values array
  129. //
  130. // The value for a given attribute are in the same position
  131. // at the attribute within the values array.  Singleton name
  132. // tokens have an empty ("") or undefined value.
  133. // 
  134. // If bStripQuotes is true, then any "outer" quotes around an
  135. // attribute value are stripped, e.g., the value in
  136. //
  137. //    NAME="bob's name"
  138. //
  139. // is returned as: bob's name.  If bStripQuotes is false, that 
  140. // value is returned as "bob's name"
  141. //
  142. // If bMakeUpper is true, all attribute/value strings are normalized
  143. // to upper case
  144. //
  145. function parseAttributes( node, bStripQuotes, bMakeUpper )
  146. {
  147.    var   tagstr         = node.outerHTML;
  148.    var   pos            = 0;
  149.    var   prevChar       = null;
  150.    var   currentChar    = null;
  151.    var   currentQuote   = null;
  152.    var   arrAttribs     = new Array();
  153.    var   arrValues      = new Array();
  154.    var   arrIdx         = 0;
  155.    var   attrib         = "";
  156.    var   value          = "";
  157.    var   bValueIsEmpty  = false;
  158.    var   bInsideQuote   = false;
  159.    var   bAccumValue    = false;
  160.    var   bAttribReady   = false;
  161.    var   bSkipToWhite   = true;  // initially true to skip "<tag "
  162.       
  163.    while( pos < node.outerHTML.length )
  164.    {
  165.       prevChar     = currentChar;
  166.       currentChar  = tagstr.charAt( pos++ );
  167.     
  168.       // Handle quote state; remember actual quote that 
  169.       // flipped the state so we match ' and " right
  170.       //                                     
  171.       if ( isQuote( currentChar ) )
  172.       {
  173.          if ( bInsideQuote )
  174.          {
  175.             if ( currentChar == currentQuote )
  176.             {
  177.                // Coming out of quoted region; turn quotes off
  178.                bInsideQuote = false;               
  179.                currentQuote = null;
  180.                if ( bStripQuotes )
  181.                {
  182.                   // Careful; make sure ATTR="" works even when we're
  183.                   // stripping quotes off values
  184.                   MM_assert( bAccumValue, MSG_ParseErrEndQuote );
  185.                   bValueIsEmpty = true;                     
  186.                   continue;
  187.                }
  188.             }                           
  189.          }
  190.          else
  191.          if ( bAccumValue && value == "" ) // only turn quotes on after '=' and
  192.          {                                 // before accumulating anything; e.g.,
  193.             // Turn quotes on              // ignore the quote in ATTR=xxx"xxx
  194.             bInsideQuote = true;
  195.             currentQuote = currentChar;
  196.             if ( bStripQuotes )
  197.                continue;
  198.          }
  199.       }
  200.       
  201.       // Handle the terminating character; write any attribute/value
  202.       // we may have been accumulating and we're done.
  203.       //
  204.       if ( !bInsideQuote && isTagEnd( currentChar ) )
  205.       {
  206.          if ( attrib != "" )
  207.          {         
  208.             arrAttribs[ arrIdx ]  = bMakeUpper ? attrib.toUpperCase() : attrib;
  209.             arrValues[ arrIdx++ ] = bMakeUpper ? value.toUpperCase() : value;
  210.             attrib = "";
  211.             value  = "";
  212.             bAttribReady = false;
  213.             bAccumValue  = false;      
  214.          }
  215.          break; 
  216.       }               
  217.       
  218.       // Accumulate characters; if bAccumValue is true, we're on the
  219.       // right side of an "=", otherwise we're on the left side or accumulating
  220.       // a singleton name token.  I don't think quoted regions make sense 
  221.       // on the left side either.
  222.       //                                   
  223.       if ( !bInsideQuote && !bAccumValue )
  224.       {
  225.          // first skip to white after tag name <xxxx 
  226.          if ( bSkipToWhite && !isWhite( currentChar ) )
  227.             continue;
  228.             
  229.          bSkipToWhite = false;              
  230.          
  231.          // Whitespace not inside quotes; if we're accumulating
  232.          // an attribute, it's ready (the whitespace terminates it);
  233.          if ( isWhite( currentChar ) )
  234.          {
  235.             bAttribReady = attrib != "";
  236.          }
  237.          else
  238.          {
  239.             // Non-white space; if we have an equals sign, switch
  240.             // over to accumulate the value
  241.             if ( currentChar == '=' )
  242.             {
  243.                bAttribReady = attrib != "";
  244.                bAccumValue  = true;
  245.                MM_assert( bAttribReady, MSG_ParseErrUnexpectedEQU );
  246.             }
  247.             else
  248.             {
  249.                // Unquoted non-white non-value -- accumulate
  250.                // as name token.  If there's a name token ready, 
  251.                // save it as a singleton first.
  252.                //
  253.                if ( bAttribReady )
  254.                {
  255.                   arrAttribs[ arrIdx++ ] = bMakeUpper ? attrib.toUpperCase() : attrib;
  256.                   attrib = "";
  257.                   bAttribReady = false;
  258.                }
  259.             
  260.                attrib += currentChar;
  261.             }
  262.          }
  263.       }
  264.       else
  265.       {
  266.          // We're accumulating a value
  267.          //
  268.          MM_assert( bAttribReady, MSG_ParseErrUnexpectedEQU );
  269.          
  270.          if ( !bInsideQuote && isWhite( currentChar ) )
  271.          {
  272.             // Swallow whitespace until we either get a value
  273.             // or we terminate
  274.             
  275.             if ( value != "" || bValueIsEmpty )
  276.             {
  277.                arrAttribs[ arrIdx ]  = bMakeUpper ? attrib.toUpperCase() : attrib;
  278.                arrValues[ arrIdx++ ] = bMakeUpper ? value.toUpperCase() : value;
  279.                attrib = "";
  280.                value  = "";
  281.                bAttribReady  = false;
  282.                bAccumValue   = false;
  283.                bValueIsEmpty = false;
  284.             }
  285.          }            
  286.          else
  287.          {
  288.             // We're inside a quote, or we're not terminated -- keep
  289.             // accumulating
  290.             // 
  291.             value += currentChar;
  292.          }
  293.       }
  294.    }
  295.    
  296.    // We're done; package up our arrays and return them
  297.    //
  298.    MM_assert( !bAccumValue, MSG_ParseErrValue );
  299.    return new Array( arrAttribs, arrValues );
  300. }
  301.  
  302. // findCombinableParent()
  303. //
  304. // Return a parent node with which the given node may have
  305. // its attributes combined with.  This routine trusts that
  306. // caller has verified that the combineTagName is a member
  307. // of combinableTagCandidates!  A combinable parent is 
  308. // a direct parent up the tree who is the parent of no
  309. // other children (which would not want to inherit the
  310. // characteristics of the given child whose attributes
  311. // will migrate up), e.g.:
  312. //
  313. // <FONT face="arial"><FONT color="blue">text</FONT></FONT>
  314. // 
  315. // and
  316. //
  317. // <FONT face="arial"><B><FONT color="blue">text</FONT></B></FONT>
  318. //
  319. // are combinable, but 
  320. //
  321. // <FONT face="arial"><B>x<FONT color"blue">text</FONT></B></FONT>
  322. //
  323. // is not as the 'x' textual child should not inherit the blue 
  324. // characteristic.  This routine recursively calls itself to 
  325. // walk the "direct" (childNodes.length == 1) parent chain.
  326. // 
  327. function findCombinableParent( node, combineTagName )
  328. {
  329.    MM_assert( arrayContains(combinableTagCandidates, combineTagName ) );
  330.    
  331.    if ( (node.parentNode != null)    &&
  332.         (node.parentNode.childNodes.length == 1) )
  333.    {
  334.       if ( combineTagName == node.parentNode.tagName )
  335.          return( node.parentNode );
  336.          
  337.       if ( node.parentNode.innerHTML == node.outerHTML ) // parent contains only this child tree
  338.          return( findCombinableParent( node.parentNode, combineTagName ) );  
  339.    }
  340.  
  341.    return null;
  342. }
  343.  
  344. // hasRedundantParent()
  345. //
  346. // Return true if the given node is redundant with a
  347. // controlling parent.  Redundant parent/children must
  348. // have identical attribute/value sets.
  349. //   
  350. function hasRedundantParent( node )
  351. {
  352.    var rc = false;
  353.    
  354.    if ( arrayContains(redundantTagCandidates,  node.tagName ) )
  355.    {      
  356.       var parent  = node.parentNode;
  357.       
  358.       // Find controlling parent
  359.       while( parent != null )
  360.       {
  361.          if ( node.tagName == parent.tagName )
  362.          {
  363.             // Compare parent and child attribute name/value pairs
  364.             var cArrs   = parseAttributes( node, true, true );
  365.             var pArrs   = parseAttributes( parent, true, true );
  366.             var cNames  = cArrs[0];
  367.             var cValues = cArrs[1];
  368.             var pNames  = pArrs[0];
  369.             var pValues = pArrs[1];
  370.  
  371.             if ( cNames.length == pNames.length && cValues.length == pValues.length )
  372.             {
  373.                cNames.sort();
  374.                pNames.sort();
  375.                cValues.sort();
  376.                pValues.sort();
  377.                
  378.                var len = cNames.length;
  379.                for( var i = 0; i < len; i++ )
  380.                {
  381.                   // note in js: undefined == undefined is true
  382.                   if ( pNames[i]  != cNames[i] || cValues[i] != pValues[i] )
  383.                      break;
  384.                }
  385.                
  386.                rc = (i == len); // if we got through everything they're the same
  387.             }
  388.             
  389.             if ( rc )  // if we're redundant, we're done
  390.                break;
  391.             
  392.             // Otherwise, if we're not actually overriding anything on this
  393.             // parent, we may still be redundant with an uber parent.  Cycle through
  394.             // the child's attributes and if none are present on parent keep going
  395.             //
  396.             var bKeepGoing = true;
  397.             for( var i = 0; i < cNames.length; i++ )
  398.             {
  399.                if ( arrayContains(pNames,  cNames[i] ) )
  400.                {
  401.                   bKeepGoing = false;
  402.                   break;
  403.                }
  404.             }            
  405.             
  406.             if ( !bKeepGoing )
  407.                break;
  408.          }
  409.       
  410.          parent = parent.parentNode;
  411.       }
  412.    }
  413.  
  414.    return rc;
  415. }
  416.  
  417. // isAllWhiteNodeSignificant()
  418. //
  419. // Given a node whose inner html is all white, this
  420. // routine examines the node's siblings and returns 
  421. // true if the whitespace is significant and false
  422. // otherwise.
  423. //
  424. function isAllWhiteNodeSignificant( node )
  425. {
  426.    var siblings   = node.parentNode.childNodes;
  427.    var nSiblings  = siblings.length;
  428.    var siblingIdx = 0;
  429.    
  430.    // If we're an only child, then we really need
  431.    // to look at uncles and aunts.  
  432.    if ( (nSiblings == 1) && (node.parentNode != null) && (node.parentNode.nodeType != Node.DOCUMENT_NODE) )
  433.       return( isAllWhiteNodeSignificant( node.parentNode ) );
  434.    
  435.    // Find self as parent's child first      
  436.    for( ; siblingIdx < nSiblings; siblingIdx++ )
  437.       if ( siblings.item( siblingIdx ) == node )
  438.          break;
  439.    
  440.    MM_assert( siblingIdx < nSiblings, MSG_ErrParentChild );
  441.  
  442.    // If sibling to the left has trailing whitespace, 
  443.    // our current all white node isn't significant.  Note
  444.    // we can just look to our immediate left rather than go
  445.    // to zero because any empty siblings to the left will
  446.    // have already been gobbled.
  447.    //
  448.    var lSibling = siblingIdx > 0 ? siblings.item( siblingIdx - 1) : null;
  449.    if ( lSibling != null )
  450.    {
  451.       if ( lSibling.nodeType == Node.TEXT_NODE )
  452.       {
  453.          if ( (lSibling.data.length > 0) &&
  454.               isWhite( lSibling.data[ lSibling.data.length - 1 ] ) )
  455.             return false;
  456.       }   
  457.       else
  458.       if ( lSibling.nodeType == Node.ELEMENT_NODE )
  459.       {
  460.          // non text left sibling
  461.          if ( (lSibling.innerHTML.length > 0) &&
  462.               isWhite( lSibling.innerHTML[ lSibling.innerHTML.length - 1 ] ) )
  463.             return false;
  464.       }
  465.       // else go on to our right to determine our significance
  466.    }
  467.       
  468.    // Now see if there's significant leading whitespace to 
  469.    // the immediate right that might render our all white 
  470.    // current node insignificant
  471.    //
  472.    var rSibling = null;
  473.    siblingIdx++;
  474.    while( siblingIdx < nSiblings )
  475.    {
  476.       rSibling = siblings.item( siblingIdx );
  477.       
  478.       if ( rSibling.nodeType == Node.TEXT_NODE )
  479.       {
  480.          // We have a textual sibling to the right; if
  481.          // this guy doesn't have leading whitespace, 
  482.          // we're significant, otherwise we're not.
  483.          if ( rSibling.data.length > 0 )
  484.             return( !isWhite( rSibling.data[0] ) );
  485.             
  486.          // else empty text node
  487.       }
  488.       else
  489.       if ( rSibling.nodeType == Node.ELEMENT_NODE )
  490.       {
  491.          // We have a non-empty non-text node to the
  492.          // right; if this guy doesn't have leading
  493.          // whitespace we're significant, otherwise not
  494.          if ( rSibling.innerHTML.length > 0 ) 
  495.             return( !isWhite( rSibling.innerHTML[0] ) );
  496.             
  497.          // else empty non-text node...
  498.       }
  499.          
  500.       siblingIdx++;
  501.    }
  502.    
  503.    // If we got here there's nothing interesting to the
  504.    // right of this all white node, so it's as if we're
  505.    // an only child.  The DOCUMENT_NODE check is just for
  506.    // safety; there shouldn't be a way to get that high on
  507.    // empty markup node removal...
  508.  
  509.    if ( node.parentNode != null && node.nodeType != Node.DOCUMENT_NODE )
  510.       return( isAllWhiteNodeSignificant( node.parentNode ) );
  511.    
  512.    // otherwise nothing left -- we really are insignificant...
  513.    return false;
  514. }
  515.  
  516. // isRemovableEmptyTag()
  517. //
  518. // Return true if this tag can be safely removed from the
  519. // document, false otherwise.
  520. //
  521. function isRemovableEmptyTag( tagNode )
  522. {
  523.    // First this tag must be an empty removal candidate with no class info
  524.    //
  525.    if ( arrayContains(emptyRemovalCandidates,  tagNode.tagName ) && !hasClassAttribute( tagNode ) )
  526.    {
  527.       // Short-circuit for named anchor tags; empty named anchors
  528.       // should be left alone
  529.       if ( "A" == tagNode.tagName && (null != tagNode.getAttribute( "NAME" )) )
  530.          return false;
  531.       
  532.       // If the innerHTML length is zero, it's empty and
  533.       // can be safely removed *unless* it's a heading
  534.       // tag -- the first empty heading tag after text 
  535.       // forces a carriage return.  
  536.       //      
  537.       if ( tagNode.innerHTML.length == 0 )
  538.       {
  539.          if ( ('H' == tagNode.tagName.charAt( 0 )) && bPreserveEmptyHeader )
  540.          {
  541.             // Preserve this empty header but gobble the next one we hit
  542.             bPreserveEmptyHeader = false;
  543.             return false;
  544.          }
  545.          
  546.          return true;
  547.       }
  548.       else
  549.       if ( isAllWhite( tagNode.innerHTML ) && !isAllWhiteNodeSignificant( tagNode ) )
  550.       {
  551.          // All empty tag candidates (generally character markup)
  552.          // spanning only whitespace can also be removed if the 
  553.          // tag is not within text, or if the tag to the right of
  554.          // text that ends in whitespace or to the left of text
  555.          // that begins with whitespace....
  556.          return true;
  557.       }
  558.    }
  559.  
  560.    return false;
  561. }
  562.  
  563. // Using a tracing image in Dreamweaver attaches up to four proprietary
  564. // attributes to the body tag. We want to remove these attributes if Remove
  565. // Dreamweaver Comments is checked.
  566. //
  567. function removeTracingAttrs()
  568. {
  569.   var bodyNode = dreamweaver.getDocumentDOM('document').body;
  570.     
  571.   //look for tracing attributes - if any are found, toggle
  572.   //the global boolean to true and remove all attributes
  573.   if (cbRemoveDWComments.checked){
  574.     if (bodyNode.getAttribute("tracingsrc") ||
  575.         bodyNode.getAttribute("tracingopacity") ||
  576.         bodyNode.getAttribute("tracingx") ||
  577.         bodyNode.getAttribute("tracingy"))
  578.    {
  579.      //remove all tracing image attributes  
  580.      bodyNode.removeAttribute("tracingsrc");
  581.      bodyNode.removeAttribute("tracingopacity");
  582.      bodyNode.removeAttribute("tracingx");
  583.      bodyNode.removeAttribute("tracingy");
  584.      bRemovedTracing=true;
  585.    }
  586.   }   
  587. }
  588.  
  589. // hasStyleAttribute()
  590. //
  591. // Return true if the given ELEMENT tag has a STYLE set
  592. //   
  593. function hasStyleAttribute( tagNode )
  594. {
  595.    return( tagNode.getAttribute( strStyleAttrib ) != null );
  596. }
  597.  
  598. // hasClassAttribute()
  599. //
  600. // Return true if the given ELEMENT tag has a CLASSID set
  601. //
  602. function hasClassAttribute( tagNode )
  603. {
  604.    return( tagNode.getAttribute( strClassAttrib ) != null );
  605. }
  606.  
  607. // loadNDWCommentOffsets()
  608. //
  609. // This callback is used by the comment removal traversal 
  610. // to push offsets of non-Dreamweaver comment nodes into
  611. // the userData variable passed by the comment removal pass
  612. //
  613. function loadNDWCommentOffsets( commentNode, userData )
  614. {
  615.    // MM_note( "Processing NDW comment:" + commentNode.data );
  616.    
  617.    // Server-side include comments of the form "<!-- #include... -->"
  618.    // should always be left alone!
  619.  
  620.    // eat up any leading white in comment data   
  621.    var i;
  622.    for( i = 0; i < commentNode.data.length; i++ )
  623.       if ( !isWhite( commentNode.data.charAt( i ) ) )
  624.          break;
  625.  
  626.    // if we have a #include skip it, otherwise push offsets for
  627.    // removal
  628.    //   
  629.    var bSkipSSIinclude = commentNode.data.substr( i, 8 ).toLowerCase() == "#include";
  630.    var bSkipSSIecho = commentNode.data.substr( i, 5 ).toLowerCase() == "#echo";
  631.    
  632.    if ( !bSkipSSIinclude && !bSkipSSIecho)
  633.       userData.push( dreamweaver.nodeToOffsets( commentNode ) );
  634.       
  635.    return true;
  636. }
  637.  
  638. // processElement()
  639. //
  640. // Process a node of ELEMENT type within the user's document
  641. // This is a callback from traverse() used during the main 
  642. // removal traversal.
  643. //
  644. function processElement( elementNode )
  645. {
  646.    // MM_note( "Processing element: " + elementNode.tagName );
  647.    // Remove specific tag(s) check
  648.    //
  649.    if ( cbRemoveTags.checked &&
  650.         arrayContains(arrTagsToRemove,  elementNode.tagName ) )
  651.    {
  652.       // MM_note( "* Removing specified tag " + elementNode.outerHTML );
  653.       if ( elementNode.outerHTML == elementNode.innerHTML )
  654.          elementNode.outerHTML = "";
  655.       else         
  656.          elementNode.outerHTML = elementNode.innerHTML;      
  657.          
  658.       numTagsRemoved++;
  659.    }           
  660.    else
  661.    {
  662.       // Don't touch tags with style information
  663.       //
  664.       if ( !hasStyleAttribute( elementNode ) ) 
  665.       {
  666.          // Empty tag check
  667.          // 
  668.  
  669.          if ( cbRemoveEmptyTags.checked && 
  670.          (isRemovableEmptyTag( elementNode )))
  671.          {
  672.             var parent = elementNode.parentNode;
  673.  
  674.             // MM_note( "* Removing empty tag: " + elementNode.outerHTML );
  675.             elementNode.outerHTML = "";
  676.             numEmptyRemoved++;
  677.  
  678.             // Small work around DW behavior -- paragraph tags with 
  679.             // children are considered "not collapsable" even if the 
  680.             // children are empty.  When we remove all empty children 
  681.             // of a p tag then, DW sticks in a   to keep the 
  682.             // remaining <p> from being collapsed -- this makes the <p> 
  683.             // then come alive in the browser layout.  So if we've just 
  684.             // zapped the last child of a p tag, rewrite the P tag without 
  685.             // the   so it remains collapsed in the browser layout.  
  686.             // Note that if the p tag originally had text or an  
  687.             // it would still have textual children after the empty tag 
  688.             // removal and would be untouched.
  689.             //
  690.             if ( parent.tagName == "P" && !(parent.hasChildNodes()) )
  691.                parent.outerHTML = "<p>";
  692.          }
  693.          // Redundant child check
  694.          //
  695.          else
  696.          if ( cbRemoveRedundant.checked &&
  697.               hasRedundantParent( elementNode ) )
  698.          {
  699.             // MM_note( "* Removing redundant tag: " + elementNode.outerHTML );
  700.             elementNode.outerHTML = elementNode.innerHTML;
  701.             numRedundantRemoved++;
  702.          }
  703.          // Child/parent coalesce check
  704.          //
  705.          else
  706.          if ( cbCombineFonts.checked &&
  707.               arrayContains(combinableTagCandidates,  elementNode.tagName ) )
  708.          {
  709.             var parent  = findCombinableParent( elementNode, elementNode.tagName );
  710.             if ( parent != null )
  711.             {
  712.                // MM_note( "* Combining font tags: " + elementNode.outerHTML );
  713.                
  714.                // Set all child attributes on parent and remove child
  715.                //
  716.                var arrs    = parseAttributes( elementNode, true, false );
  717.                var attribs = arrs[0];
  718.                var values  = arrs[1];
  719.                
  720.                for( var i = 0; i < attribs.length; i++ )
  721.                   parent.setAttribute( attribs[i], values[i] ); // The value part 
  722.                                                                 // here may be null
  723.                elementNode.outerHTML = elementNode.innerHTML;
  724.                numFontsCombined++;
  725.             }
  726.          }
  727.          // Dreamweaver comment check -- dreamweaver comments
  728.          // come back to us as element nodes rather than comment nodes
  729.          else
  730.          if ( cbRemoveDWComments.checked && 
  731.               arrayContains(arrDWCommentTags,  elementNode.tagName ) )
  732.          {
  733.             // MM_note( "Removing DW comment: " + elementNode.tagName );
  734.             dreamweaver.editLockedRegions(true);
  735.             elementNode.outerHTML = elementNode.innerHTML;
  736.             numCommentsRemoved++;
  737.          }
  738.       }
  739.    }
  740.    
  741.    return true; // continue traverse
  742. }
  743.  
  744. // emptyHeaderStateTextHandler()
  745. //
  746. // This text node callback is used by pass two to flip
  747. // the global bPreserveEmptyHeader state to true -- we
  748. // just encountered text, so the next empty header 
  749. // found will force a carriage return and thus can't
  750. // be removed.  Empty headers after that however can
  751. // be removed until the next piece of text is encountered...
  752. //
  753. function emptyHeaderStateTextHandler( node )
  754. {
  755.    bPreserveEmptyHeader = true;
  756.    return true;
  757. }
  758.  
  759. // traverse()
  760. //
  761. // Do a recursive depth-first traversal of the user's 
  762. // document starting from the given node.  
  763. //
  764. // Callers provide up to three callback functions which
  765. // accept a node argument, one each (or the same one)
  766. // to process nodes of ELEMENT, TEXT, or COMMENT type.
  767. // At least one callback function is required.
  768. //
  769. // The handlers may stop the traversal by returning false;
  770. // returning true will continue the traversal to its 
  771. // completion.
  772. //
  773. // A fourth argument, a handle to a some user variable to
  774. // be passed on to each callback, may also be provided.
  775. //
  776. function traverse( node, fElementHandler ) // optional: fTextHandler, fCommentHandler, userData )
  777. {
  778.    var fTextHandler  = traverse.arguments.length >= 3 ? traverse.arguments[2] : null;  
  779.    var fCmmtHandler  = traverse.arguments.length >= 4 ? traverse.arguments[3] : null;
  780.    var userData      = traverse.arguments.length >= 5 ? traverse.arguments[4] : null;
  781.    var children      = node.childNodes;
  782.    var nChildren     = children.length;
  783.    var bContinue     = true;
  784.    var current       = null;
  785.    
  786.    for( var i = 0; bContinue && (i < nChildren); i++ )
  787.    {
  788.       current = children.item( i );
  789.    
  790.       // descend to any children first
  791.       if ( current.hasChildNodes() )
  792.          traverse( current, fElementHandler, fTextHandler, fCmmtHandler, userData );
  793.  
  794.       // process current node
  795.       switch( current.nodeType )
  796.       {
  797.          case Node.ELEMENT_NODE:
  798.             if ( userData != null )
  799.                bContinue = fElementHandler( current, userData );
  800.             else
  801.                bContinue = fElementHandler( current );
  802.             break;
  803.                
  804.          case Node.COMMENT_NODE:
  805.             if ( fCmmtHandler != null )
  806.                if ( userData != null )
  807.                   bContinue = fCmmtHandler( current, userData );
  808.                else
  809.                   bContinue = fCmmtHandler( current );
  810.             break;
  811.             
  812.          case Node.TEXT_NODE:
  813.             if ( fTextHandler != null )
  814.                if ( userData != null )
  815.                   bContinue = fTextHandler( current, userData )
  816.                else
  817.                   bContinue = fTextHandler( current )
  818.             break;
  819.          
  820.          case Node.DOCUMENT_NODE:
  821.          default:
  822.              MM_error( MSG_UnknownNodeType, current.nodeType );
  823.       }
  824.    }
  825. }
  826.  
  827. // doPassOne()
  828. //
  829. // Pass one does cleanup based on the HTML source string for 
  830. // the user's document; currently that means comment and extra 
  831. // whitespace removal.
  832. //
  833. // BW 8/17/98 Removed "remove extra whitespace" option for 
  834. //            performance reasons
  835. //
  836. function doPassOne()
  837. {
  838.    if ( cbRemoveComments.checked )  // pass one options
  839.    {
  840.       var htmlstr = dreamweaver.getDocumentDOM( 'document' ).documentElement.outerHTML;
  841.       var htmlpos = 0;                                    
  842.       var htmlarr = new Array(); // array to save newing of intermediate
  843.                                  // string copies of doc
  844.  
  845.       // To remove comments, traverse over the entire DOM gathering
  846.       // offsets into the HTML source of the comments to be removed, 
  847.       // then remove those comments from the HTML source string.
  848.       //
  849.       var root           = dreamweaver.getDocumentDOM('document');
  850.       var commentOffsets = new Array();
  851.       var stubCallback   = new Function( "node", "userData", "return true;" );
  852.  
  853.       if ( root != null && root.hasChildNodes() )
  854.          traverse( root
  855.                  , stubCallback 
  856.                  , stubCallback
  857.                  , loadNDWCommentOffsets
  858.                  , commentOffsets ); 
  859.                  
  860.       // Now use offsets to delete sections of text from
  861.       // within the document source string.  
  862.       //
  863.       if ( commentOffsets.length > 0 )
  864.       {                 
  865.          var lastpos = 0;
  866.          for( var i = 0; i < commentOffsets.length; i++ )
  867.          {
  868.             htmlarr[htmlpos++] = htmlstr.substring( lastpos
  869.                                                   , commentOffsets[i][0] );
  870.             lastpos = commentOffsets[i][1];
  871.             numCommentsRemoved++;
  872.          }
  873.          
  874.          htmlarr[htmlpos++] = htmlstr.substring( lastpos );
  875.       }
  876.  
  877.       if ( htmlarr.length > 0 )
  878.          dreamweaver.getDocumentDOM( 'document' ).documentElement.outerHTML = htmlarr.join("");
  879.    }
  880. }
  881.  
  882. // doPassTwo()
  883. //
  884. // Pass two does cleanup on DOM objects as appropriate over the
  885. // course of traversing the DOM heirarchy.  The actual work in this 
  886. // pass is done in the processElement() callback.
  887. //
  888. function doPassTwo()
  889. {   
  890.    // Load up comma-separated list of tags to remove if any; warn
  891.    // if option is checked but no tags specified
  892.    //      
  893.    arrTagsToRemove = dreamweaver.getTokens( tbTagsToRemove.value.toUpperCase(), ", " );
  894.    if ( cbRemoveTags.checked && arrTagsToRemove.length == 0 )
  895.       MM_error( MSG_NoTagsToRemove ); 
  896.       
  897.    // Traverse document, processing leaves
  898.    //
  899.    var root = dreamweaver.getDocumentDOM('document');
  900.    
  901.    if ( root != null && root.hasChildNodes() )
  902.    {
  903.       traverse( root
  904.               , processElement 
  905.               , emptyHeaderStateTextHandler ); // no comment handler for this pass
  906.               
  907.       // and finally attempt to remove tracingsrc attributes
  908.       // in body tag 
  909.       //
  910.       removeTracingAttrs();
  911.    }        
  912.    else
  913.       MM_error( MSG_ErrEmptyDoc );
  914.    
  915. }
  916.  
  917. // cleanUpDocument()
  918. //
  919. // Main routine for performing clean up when user hits OK.
  920. // Clean up is done in three passes:
  921. //
  922. // Pass 1: Clean up certain items based on the entire HTML 
  923. //         document as a string
  924. // Pass 2: Clean up certain items while traversing the DOM
  925. //
  926. function cleanUpDocument()
  927. {
  928.    // Set up logging particulars
  929.    //
  930.    if ( cbShowLog.checked )
  931.    {  
  932.       MM_enableLogging();
  933.       MM_clearLog();
  934.    }
  935.    else {
  936.       MM_disableLogging();
  937.    }
  938.  
  939.    // Do cleanup in two passes -- the first pass , the second pass 
  940.    // cleans up certain items based on a hierarchy traversal of the DOM.
  941.    //
  942.    MM.setBusyCursor();
  943.    doPassOne();
  944.    doPassTwo();
  945.    MM.clearBusyCursor();
  946.    finalize();
  947. }
  948.  
  949. // initialize()
  950. //
  951. // This is called on BODY onLoad; initialize all script globals
  952. //
  953. function initialize()
  954. {
  955.    // Counters for logging output
  956.    //
  957.    numEmptyRemoved      = 0;
  958.    numRedundantRemoved  = 0;
  959.    numTagsRemoved       = 0;
  960.    numCommentsRemoved   = 0;
  961.    numFontsCombined     = 0;
  962.    bRemovedTracing      = false;
  963.  
  964.    arrTagsToRemove.length = 0; // Empty array
  965.  
  966.    strClassAttrib       = "CLASS";
  967.    strStyleAttrib       = "STYLE";
  968.  
  969.    // The following tags represent the tag names of Dreamweaver-
  970.    // specific comments, which are processed through the Dreamweaver
  971.    // JS API/DOM as named element nodes rather than comment nodes
  972.    //
  973.    arrDWCommentTags.push ( "MM:EDITABLE"      
  974.                          , "MM:LIBITEM"       // variable library item (currently unused)
  975.                          , "MM:TEMPLATE"      
  976.                          , "MM:UNLOCKATTRS"   // currently unused
  977.                          , "{#CUSTOMOBJ}"
  978.                          , "{#MEINLINE}"      // used by Japanese DW
  979.                          , "{#LIBITEM}" );
  980.  
  981.    // The following tags can be harmlessly removed from the user's
  982.    // document if they're empty.  Note that the Heading tags are
  983.    // not always safe and require special further handling; see
  984.    // isEmptyRemoveableTag(). 
  985.    //
  986.    emptyRemovalCandidates.push ( "H1", "H2", "H3", "H4", "H5", "H6"
  987.                                , "TT", "I", "B", "U", "STRIKE", "BIG"
  988.                                , "SMALL", "SUB", "SUP", "EM", "STRONG"
  989.                                , "DFN", "CODE", "SAMP", "KBD", "VAR"
  990.                                , "CITE", "XMP", "BLINK"
  991.                                , "ADDRESS"
  992.                                , "A"
  993.                                , "FONT"
  994.                                , "SPAN"
  995.                                , "TABLE"
  996.                                , "BLOCKQUOTE"
  997.                                , "LI", "OL", "UL"
  998.                                , "DD", "DT", "DL"
  999.                                , "DIR", "MENU"
  1000.                                , "DIV", "CENTER" );
  1001.                                  
  1002.    // These tags can be safely removed if they're redundant
  1003.    // with their immediate parent, i.e., this tags have
  1004.    // no nesting semantics.
  1005.    //
  1006.    redundantTagCandidates.push( "TT", "I", "B", "U", "STRIKE", "BIG"
  1007.                               , "SMALL", "SUB", "SUP", "EM", "STRONG"
  1008.                               , "DFN", "CODE", "SAMP", "KBD", "VAR"
  1009.                               , "CITE", "XMP"
  1010.                               , "FONT"
  1011.                               , "CENTER"
  1012.                               , "SPAN" );
  1013.  
  1014.    // These tags can be safely coalesced with parents with identical
  1015.    // regions of influence.  Currently this is only done for FONT tags.
  1016.    //                                     
  1017.    combinableTagCandidates.push( "FONT" );
  1018.    
  1019.  
  1020.    // Global used by pass two to indicate if the next empty 
  1021.    // header we encounter should be preserved -- the first
  1022.    // empty header after text is significant as a carriage
  1023.    // return is forced; after that they can be gobbled until
  1024.    // there's more text.
  1025.    //
  1026.    bPreserveEmptyHeader = false;
  1027.                                           
  1028.                                  
  1029.    // And finally reference actual form element names 
  1030.    // here once
  1031.    //
  1032.    with( document.optionsForm )
  1033.    {
  1034.       cbRemoveEmptyTags       = removeEmptyTags;
  1035.       cbRemoveRedundant       = removeRedundantChildren;
  1036.       cbRemoveComments        = removeNDWComments;
  1037.       cbRemoveDWComments      = removeDWComments;
  1038.       cbRemoveTags            = removeTag;
  1039.       cbCombineFonts          = combineFonts;
  1040.       cbShowLog               = showLog;
  1041.       tbTagsToRemove          = tagsToRemove;
  1042.    }
  1043. }
  1044.  
  1045.  
  1046.  
  1047. function finalize()
  1048. {
  1049.    // Show what we did if show log is enabled
  1050.    //
  1051.    if ( cbShowLog.checked )
  1052.    {
  1053.       MM_note( MSG_TrcSummaryHeader );
  1054.       
  1055.       var bDidSomething = (numEmptyRemoved > 0)      ||
  1056.                           (numRedundantRemoved > 0)  ||
  1057.                           (numTagsRemoved > 0)       ||
  1058.                           (numCommentsRemoved > 0)   ||
  1059.                           (numFontsCombined > 0)     ||
  1060.                           (bRemovedTracing);
  1061.                           
  1062.       if ( bDidSomething )
  1063.       {                             
  1064.          if ( numEmptyRemoved > 0 ) 
  1065.             MM_note( MSG_TrcEmptyRemoved, numEmptyRemoved );
  1066.          if ( numRedundantRemoved > 0 )   
  1067.             MM_note( MSG_TrcRedundantRemoved, numRedundantRemoved );
  1068.          if ( numTagsRemoved > 0 )   
  1069.             MM_note( MSG_TrcTagsRemoved, numTagsRemoved );
  1070.          if ( numCommentsRemoved > 0 )   
  1071.             MM_note( MSG_TrcCommentsRemoved, numCommentsRemoved );
  1072.          if ( numFontsCombined > 0 )   
  1073.             MM_note( MSG_TrcFontsCombined, numFontsCombined );
  1074.          if ( bRemovedTracing )   
  1075.             MM_note( MSG_TracingAttrsRemoved );
  1076.       }
  1077.       else {
  1078.          MM_note( MSG_TrcDidNothing );
  1079.       }
  1080.       MM_showLog();
  1081.    }
  1082.  
  1083.    window.close();         
  1084. }
  1085.